﻿using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Linq;
using System.Reflection;
using System.Threading;
using CoreEx.Common.Linq;
using CoreEx.Database.Linq.Translators;
using CoreEx.Database.Metadata;
using CoreEx.Database.Query.Expressions;
using LinFu.IoC.Configuration;
using LinFu.IoC.Interfaces;
using LinFu.IoC;
using CoreEx.Common.Extensions;
namespace CoreEx.Database.Linq.Implementation
{
    [Implements(typeof(IQueryTranslator))]
    public class QueryTranslator : QueryTranslatorBase
    {
        [ThreadStatic]
        private static IDictionary<Type, IList<IRelationLoadTarget>> _loadOptions;
        
        
        /// <summary>
        /// Initializes a new instance of the <see cref="QueryTranslator"/> class.
        /// </summary>
        /// <param name="connectionName">The name of the database connection.</param>
        public QueryTranslator(string connectionName) : base(connectionName)
        {
            
        }



        /// <summary>
        /// Gets the <see cref="IExpressionHandler{TExpression}"/> that is reponsible for handling binary expressions.
        /// </summary>
        protected IExpressionHandler<BinaryExpression> BinaryExpressionHandler { get; private set; }



        public override TranslateResult Translate(Expression expression)
        {
            expression = Rewrite(expression);
            Visit(expression);
            return TranslateResult;
        }

        public override TranslateResult Translate(MethodCallExpression methodCallExpression, MappedProperty outerMappedProperty)
        {
            var elementType = outerMappedProperty.PropertyMappingInfo.Property.PropertyType.GetGenericElementType();
            
            DbQuery<DbSelectExpression> subQuery = CreateQuery(elementType);

            if (ContainsLamdaExpression(methodCallExpression))
                subQuery.Where(Visit(methodCallExpression.Arguments[1].StripQuotes()));

            subQuery.QueryExpression.ProjectionExpression = ExpressionFactory.Constant(1);
            subQuery.QueryExpression.IsSubQuery = true;

            var foreignKeyInfo = outerMappedProperty.PropertyMappingInfo.ForeignKeyInfo;
            var thisAlias = TranslateResult.TableSources.First().Value.Alias;
            var outerAlias = outerMappedProperty.TableSource.Alias;

            foreach (var foreignkeyColumnInfo in foreignKeyInfo.Columns)
            {
                subQuery.Where(
                    ExpressionFactory.Column(thisAlias,
                                             foreignkeyColumnInfo.ForeignColumnInfo.ColumnName) ==
                    ExpressionFactory.Column(outerAlias,
                                             foreignkeyColumnInfo.PrimaryColumnInfo.ColumnName));
            }

            TranslateResult.SelectQuery = subQuery;
            
            return TranslateResult;
        }

        private static bool ContainsLamdaExpression(MethodCallExpression methodCallExpression)
        {
            return methodCallExpression.Arguments.Count == 2 &&
                   methodCallExpression.Arguments[1].StripQuotes() is LambdaExpression;
        }


        protected override DbExpression VisitMemberInit(MemberInitExpression memberInitExpression)
        {
            throw new NotImplementedException();
        }

        protected override DbExpression VisitConditional(ConditionalExpression conditionalExpression)
        {
            throw new NotImplementedException();
        }

        protected override DbExpression VisitUnary(UnaryExpression expression)
        {
            if (expression.Type == typeof(bool) && expression.NodeType == ExpressionType.Not)
                return ExpressionFactory.Not(Visit(expression.Operand));

            return Visit(expression.Operand);
        }

        protected override DbExpression VisitMethodCall(MethodCallExpression methodCallExpression)
        {
            switch (methodCallExpression.Method.Name)
            {
                case "Where" :
                    return HandleWhere(methodCallExpression);
                case "Any":
                    return HandleAny(methodCallExpression);
                case "First":
                    return HandleFirst(methodCallExpression);
                case "Join":
                    return HandleJoin(methodCallExpression);
                case "Select":
                    return HandleSelect(methodCallExpression);
                default:
                    throw new NotSupportedException(string.Format("Method {0} is not supported", methodCallExpression.Method.Name));
            }
        }

        private DbExpression HandleSelect(MethodCallExpression methodCallExpression)
        {
            var dbSelectQuery = (DbQuery<DbSelectExpression>)Visit(methodCallExpression.Arguments[0]);

            var lamdbaExpression = (LambdaExpression)methodCallExpression.Arguments[1].StripQuotes();             
            var test = lamdbaExpression.Replace<MemberExpression>(me => true, me => Expression.Constant("SomeAdress"));
            TranslateResult.Projection = (LambdaExpression)test;
            var memberExpression = lamdbaExpression.Find<MemberExpression>(m => true).FirstOrDefault();
            var columnExpression = Visit(memberExpression);

            dbSelectQuery.QueryExpression.ProjectionExpression = columnExpression;


            return dbSelectQuery;            
        }


        protected override DbExpression VisitConstant(ConstantExpression expression)
        {
             if (expression.IsQueryable())            
                return CreateQuery(((IQueryable)expression.Value).ElementType);
            return ExpressionFactory.Constant(expression.Value);        
        }

        protected override DbExpression VisitMemberAccess(MemberExpression expression)
        {
            return CreateColumnExpression(expression);
        }

        protected override DbExpression VisitBinary(BinaryExpression expression)
        {
            return BinaryExpressionHandler.HandleExpression(expression,Visit);
        }

        protected override DbExpression VisitParameter(ParameterExpression expression)
        {
            throw new NotImplementedException();
        }

        protected override DbExpression VisitNew(NewExpression expression)
        {
            throw new NotImplementedException();
        }


        protected virtual DbExpression HandleWhere(MethodCallExpression methodCallExpression)
        {
            var dbSelectQuery = (DbQuery<DbSelectExpression>)Visit(methodCallExpression.Arguments[0]);
            var lambdaExpression = methodCallExpression.Arguments[1].StripQuotes();
            var whereClause = Visit(lambdaExpression);            
            return dbSelectQuery.Where(whereClause);
        }

        private DbExpression HandleJoin(MethodCallExpression methodCallExpression)
        {
            var dbSelectQuery = (DbQuery<DbSelectExpression>)Visit(methodCallExpression.Arguments[0]);
            var constantExpression = (ConstantExpression) methodCallExpression.Arguments[1];
            var outerTableSource = CreateTableSource(((IQueryable) constantExpression.Value).ElementType);
            var outerKeySelector = methodCallExpression.Arguments[2].StripQuotes();
            var outerExpression = Visit(outerKeySelector);
            var innerKeySelector = methodCallExpression.Arguments[3].StripQuotes();
            var innerExpression = Visit(innerKeySelector);
            var conditionExpression = ExpressionFactory.Equal(outerExpression, innerExpression);
            

            //dbSelectQuery.LeftOuterJoin(outerTableSource.TableExpression,)

            return null;
        }

        protected virtual DbExpression HandleAny(MethodCallExpression methodCallExpression)
        {
            var subQueryTranslator = ServiceContainer.GetService<IQueryTranslator>(TranslateResult.ConnectionName);

            var memberExpression = (MemberExpression)methodCallExpression.Arguments[0];
            var outerTableSource = GetTableSource(memberExpression);
            var outerPersistentMember = outerTableSource.MappedProperties[(PropertyInfo)memberExpression.Member];

            var subQueryResult = subQueryTranslator.Translate(methodCallExpression, outerPersistentMember);
            var expression = ExpressionFactory.Exists(subQueryResult.SelectQuery);
            return expression;
        }

        protected virtual DbExpression HandleFirst(MethodCallExpression methodCallExpression)
        {
            var dbSelectQuery = (DbQuery<DbSelectExpression>)Visit(methodCallExpression.Arguments[0]);
            return dbSelectQuery.Take(1);
        }

        protected DbQuery<DbSelectExpression> CreateQuery(MemberInfo member)
        {
            TableSource rootTableSource = CreateTableSource(member);

            var tableExpression = rootTableSource.TableExpression;
            CreateProjectionForTableSource(rootTableSource);
            TranslateResult.RootTableSource = rootTableSource;
            
            var selectQuery =  DbQueryFactory.Select(rootTableSource.Projection.DbExpression)
                .From(tableExpression);
            TranslateResult.SelectQuery = selectQuery;
            return selectQuery;
        }


        private void CreateProjectionForTableSource(TableSource tableSource)
        {
            var mappedProperties = tableSource.MappedProperties.Select(pm => pm.Value.PropertyMappingInfo);
            var projection = new Projection(tableSource);
            foreach (var mappedProperty in mappedProperties.Where(p => p.RelationType == CardinalityType.None))
            {
                var columnName = mappedProperty.ColumnInfo.ColumnName;
                var columnAlias = TranslateResult.GetUniqueColumnName(columnName);
                var columnExpression = CreateAliasedColumnExpression(columnName, columnAlias, tableSource.Alias);
                var projectedColumn = new ProjectedColumn
                {
                    TargetProperty = mappedProperty.Property,
                    ColumnName = columnAlias,
                    DbExpression = columnExpression
                };
                projection.ProjectedColumns.Add(projectedColumn);
            }
            tableSource.Projection = projection;
        }


        private static DbExpression CreateAliasedColumnExpression(string columnName, string columnAlias, string columnPrefix)
        {
            var columnExpression = ExpressionFactory.Column(columnPrefix, columnName);
            if (columnAlias != columnName)
                return ExpressionFactory.Alias(columnExpression, columnAlias);
            return columnExpression;
        }


        private TableSource CreateTableSource(MemberInfo memberInfo)
        {
            var typeMappingInfo = TypeInfoMapper.MapFrom(memberInfo.GetMemberType().GetGenericElementType());
            var alias = AliasProvider.GetAliasFor(memberInfo);
            var tableSource = new TableSource(typeMappingInfo)
            {
                TargetMember = memberInfo,
                Alias = alias,
                TableExpression =
                ExpressionFactory.Table(typeMappingInfo.TableInfo.TableName, alias),                
                TranslateResult = this.TranslateResult
            };
            //Projection projection = CreateProjectionFromTableSource(tableSource);
            //tableSource.Projection = projection;
            TranslateResult.TableSources.Add(memberInfo, tableSource);
            return tableSource;
        }


        /// <summary>
        /// Returns the <see cref="TableSource"/> that represents a table in the final query 
        /// that is needed to navigate the target member expression.
        /// </summary>
        /// <param name="targetMemberExpression">The <see cref="MemberExpression"/> for which to return a <see cref="TableSource"/> instance.</param>
        /// <remarks>
        /// This method will create the necessary join expressions that is needed to access the database column 
        /// that the <paramref name="targetMemberExpression"/> represents.
        /// </remarks>
        /// <returns><see cref="TableSource"/></returns>
        protected virtual TableSource GetTableSource(MemberExpression targetMemberExpression)
        {
            var containingExpression = targetMemberExpression.Expression;
            return GetTableSource(containingExpression);
        }

        protected virtual TableSource GetTableSource(Expression expression)
        {                        
            if (expression.NodeType == ExpressionType.Call)
            {
                
                //A member is accessed off a method call
                //Create a new Table Source that represents the sub query (derived table) 


                    var subQueryTranslator = ServiceContainer.GetService<ISubQueryTranslator>(string.Empty, TranslateResult.ConnectionName);
                var subQueryTranslateResult = subQueryTranslator.Translate((MethodCallExpression)expression, TranslateResult);
                
                //CreateTableSource()

                return null;
            }

            if (expression.NodeType == ExpressionType.MemberAccess)
            {                
                var memberExpression = (MemberExpression)expression;
                if (!TranslateResult.TableSources.ContainsKey(memberExpression.Member))
                    return CreateTableSourceForMemberExpression(memberExpression);
            }

            if (expression.NodeType == ExpressionType.Parameter)
            {
                var parameterExpression = (ParameterExpression) expression;
                return CreateTableSourceForParameterExpression(parameterExpression);
            }

            throw new ArgumentOutOfRangeException("expression", "Unknown expression");
            
        }

        protected virtual TableSource CreateTableSourceForMemberExpression(MemberExpression memberExpression)
        {
            TableSource tableSource;
            if (TranslateResult.TableSources.Count() == 0)
                tableSource = CreateRootTableSource(memberExpression);
            else
                tableSource = CreateTableSource(memberExpression.Member);

            //We need to join this member to the outer tablesource
            TableSource outerTableSource = GetOuterTableSourceForMemberExpression(memberExpression);
            var thisAlias = AliasProvider.GetAliasFor(memberExpression.Member);
            var joinExpression = CreateJoinExpression(memberExpression, outerTableSource, thisAlias);                    
            tableSource.TableExpression = joinExpression;
            outerTableSource.JoinedTableSources.Add(tableSource);
            return tableSource;
        }

        protected virtual TableSource GetOuterTableSourceForMemberExpression(MemberExpression memberExpression)
        {
            return GetTableSource(memberExpression.Expression);
        }

        private TableSource CreateRootTableSource(MemberExpression memberExpression)
        {
            var rootType = memberExpression.Type.GetGenericElementType();
            CreateQuery(rootType);
            TableSource tableSource = TranslateResult.RootTableSource;
            return tableSource;
        }

        protected virtual TableSource CreateTableSourceForParameterExpression(ParameterExpression expression)
        {            
            return TranslateResult.TableSources[expression.Type];
        }

        protected virtual DbExpression CreateJoinExpression(MemberExpression memberExpression, TableSource outerTableSource, string alias)
        {
            var outerAlias = outerTableSource.Alias;
            var outerPropertyMappingInfo = outerTableSource.MappedProperties[(PropertyInfo)memberExpression.Member].PropertyMappingInfo;
            var foreignKeyInfo = outerPropertyMappingInfo.ForeignKeyInfo;
            var cardinality = outerPropertyMappingInfo.RelationType;

            DbExpression joinCriteraExpression = null;
            foreach (var foreignKeyColumnInfo in foreignKeyInfo.Columns)
            {
                DbExpression equalsExpression = null;

                if (cardinality == CardinalityType.OneToOne)
                {
                    equalsExpression = ExpressionFactory.Column(outerAlias, foreignKeyColumnInfo.PrimaryColumnInfo.ColumnName) ==
                                       ExpressionFactory.Column(alias, foreignKeyColumnInfo.ForeignColumnInfo.ColumnName);
                }

                if (cardinality == CardinalityType.ManyToOne)
                {
                    equalsExpression =
                        ExpressionFactory.Column(outerAlias, foreignKeyColumnInfo.ForeignColumnInfo.ColumnName) ==
                        ExpressionFactory.Column(alias, foreignKeyColumnInfo.PrimaryColumnInfo.ColumnName);

                }

                if (joinCriteraExpression.IsNull())
                    joinCriteraExpression = equalsExpression;
                else
                    joinCriteraExpression = joinCriteraExpression & equalsExpression;


            }


            DbExpression joinExpression;
            if (cardinality == CardinalityType.OneToOne)
                joinExpression = ExpressionFactory.MakeJoin(DbJoinExpressionType.LeftOuterJoin,
                                                            ExpressionFactory.Table(foreignKeyInfo.Table.TableName, alias),
                                                            joinCriteraExpression);
            else
            {
                joinExpression = ExpressionFactory.MakeJoin(DbJoinExpressionType.LeftOuterJoin,
                                                            ExpressionFactory.Table(
                                                                foreignKeyInfo.PrimaryTable.TableName, alias),
                                                            joinCriteraExpression);
            }
            
            TranslateResult.SelectQuery.From(joinExpression);
                                    
            return joinExpression;
        }


        private DbExpression CreateColumnExpression(MemberExpression memberExpression)
        {
            var tableSource = GetTableSource(memberExpression);
            var columnPrefix = tableSource.Alias;
            var columnName = tableSource.MappedProperties[(PropertyInfo)memberExpression.Member].PropertyMappingInfo.ColumnInfo.ColumnName;
            var columnExpression = ExpressionFactory.Column(columnPrefix, columnName);
            return columnExpression;
        }

        public override void Initialize(IServiceContainer source)
        {
            base.Initialize(source);
            BinaryExpressionHandler = source.GetService<IExpressionHandler<BinaryExpression>>();
            
        }
    }
}
